﻿using HeatConductivityEquationLib;
using IntegralLib;
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using System.Xml.Serialization;

namespace WinHeatTransfer
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        #region === members===

        HeatConductivityEquation _heatConductivity;
        readonly string _storage = "materials.xml";

        const int _markerCount = 11;
        const double _absZero = 273;
        double _mint = _absZero;
        double _maxt = 300 + _absZero;
        readonly double _length = 1;

        Timer _timer1 = null;
        DateTime _startTime;
        TimeSpan _timeSpan = new TimeSpan(0);

        Timer _timer2 = null;
        const double _cTimerInterval = 1000;
        const double _cTimeStep = _cTimerInterval / 1000;
        const double _cStartTime = 0;
        const double _cIntegralLimit = 5;
        double _curTime;

        Bitmap _bitmap;
        string _lastError = string.Empty;
        EnumWinStatus _enumWinStatus = EnumWinStatus.Stoped;

        List<Color> _colors;
        int[] _indexes;

        System.Globalization.NumberFormatInfo _nfi = null;
        PointF[] _indexesSource = null;
        PointF[] _indexesRunTime = null;
        int _curSourceIndex = -1;

        const int _cyMargin = 20;
        const int _cRs = 3;
        bool _bBlockEvent = false;

        #endregion

        #region === private ===

        PointF[] GetSourceIndexes(Function f, int markerCount)
        {
            // прямоугольник для функций
            const int cyMargin = 20;
            int yOffset = Convert.ToInt32(0.75 * pictureBox1.Height);
            Rectangle boundRect = new Rectangle((pictureBox1.Width - KernelLength) / 2, cyMargin, KernelLength, yOffset - 18 - cyMargin - 4);
            PointF[] points = new PointF[markerCount];

            for (int i = 0; i < markerCount; i++)
            {
                float x = Convert.ToSingle(_length) / (markerCount - 1) * i - Convert.ToSingle(_length / 2);
                double u = f(x);

                float xWin = Convert.ToSingle(KernelLength * i) / (markerCount - 1) + (pictureBox1.Width - KernelLength) / 2;
                float yWin = Convert.ToSingle(-boundRect.Height / (_maxt - _mint) * (u - _mint) + boundRect.Bottom);

                points[i] = new PointF(xWin, yWin);
            }

            return points;
        }

        int KernelLength => pictureBox1.Width - 80;

        Rectangle GetBoundRect()
        {
            int yOffset = Convert.ToInt32(0.75 * pictureBox1.Height);
            return new Rectangle((pictureBox1.Width - KernelLength) / 2, _cyMargin, KernelLength, yOffset - 18 - _cyMargin - 4);
        }

        double Lines(float[] X, float[] Y, float X0)
        {
            int count = X.Length;

            float[] A = X;
            float[] B = Y;

            if (X0 <= A[0]) // X0 не больше min(X)                
            {
                return B[0];
            }
            else if (X0 >= A[count - 1]) // X0 не меньше max(X)
            {
                return B[count - 1];
            }
            else // X0 находится в диапазоне значений элементов X                
            {
                float Yc = 0;

                int N1 = 0;
                int N2 = count - 1;

                while (N2 - N1 >= 1)
                {
                    int n = (N1 + N2) >> 1;

                    if (A[n] <= X0 && X0 <= A[n + 1])
                    {
                        Yc = B[n] + (B[n + 1] - B[n]) * (X0 - A[n]) / (A[n + 1] - A[n]);
                        break;
                    }
                    else if (X0 < A[n])
                    {
                        N2 = n;
                    }
                    else
                    {
                        N1 = n;
                    }
                }

                return Convert.ToDouble(Yc);
            }
        }

        PointF ToReal(Rectangle bound, PointF point)
        {
            double x = _length / bound.Width * (point.X - bound.Left) - _length / 2;
            double y = (_maxt - _mint) / bound.Height * (bound.Bottom - point.Y) + _mint;

            return new PointF(Convert.ToSingle(x), Convert.ToSingle(y));
        }

        PointF[] ConvertIndexesToReal()
        {
            Rectangle bound = GetBoundRect();
            return _indexesSource.Select(p => ToReal(bound, p)).ToArray();
        }

        double Plateau(double x)
        {
            double cPlateau = _length / 5;
            return (-cPlateau < x && x < cPlateau) ? _maxt : _mint;
        }

        Function HeatFunction0
        {
            get 
            {
                PointF[] points = ConvertIndexesToReal();
                float[] X = points.Select(p => p.X).ToArray();
                float[] Y = points.Select(p => p.Y).ToArray();

                return x0 => Lines(X, Y, Convert.ToSingle(x0));
            }
        }

        List<Color> CreatePalette()
        {
            Palette palette = new Palette();

            palette.AddBaseColor(Color.Black);
            palette.AddBaseColor(Color.DarkRed);
            palette.AddBaseColor(Color.Red);
            palette.AddBaseColor(Color.Yellow);

            return palette.CreatePalette();
        }

        void Next()
        {
            _curTime += _cTimeStep;
            ComputeColorIndexes();
        }

        void ComputeColorIndexes()
        {
            for (int i = 0; i < _indexes.Length; i++)
            {
                double x = _length / (_indexes.Length - 1) * i - _length / 2;
                double u = _heatConductivity.HeatConductivity(x, _curTime);

                int index = Convert.ToInt32((_colors.Count - 1) / (_maxt - _mint) * (u - _mint));

                if (index >= _colors.Count)
                    index = _colors.Count - 1;

                if (index < 0)
                    index = 0;

                _indexes[i] = index;
            }
        }

        void RenderMarker(Graphics g, float x, float y, string text)
        {
            Font font = new Font("Arial", 8.0f, FontStyle.Regular);
            SizeF szf = g.MeasureString(text, font);

            g.DrawLine(Pens.Black, x, y + 4, x, y + 12);
            g.DrawString(text, font, Brushes.Black, x - szf.Width / 2, y + 14);
        }

        void RenderLenMarkers(Graphics g, float x, float y, string text)
        {
            Font font = new Font("Arial", 8.0f, FontStyle.Regular);
            SizeF szf = g.MeasureString(text, font);

            g.DrawLine(Pens.Black, x, y - 4, x, y - 12);
            g.DrawString(text, font, Brushes.Black, x - szf.Width / 2, y - 14 - szf.Height);
        }

        void RenderKernel(Graphics g, List<Color> colors, int[] indexes)
        {
            const int cYShift = 20;
            int xCur = (pictureBox1.Width - KernelLength) / 2;
            int yOffset = Convert.ToInt32(0.75 * pictureBox1.Height);

            foreach (int index in indexes)
            {
                g.DrawLine(new Pen(colors[index]), xCur, yOffset + cYShift, xCur, yOffset + cYShift + 24);
                xCur++;
            }

            double[] u = new double[_markerCount];

            for (int i = 0; i < _markerCount; i++)
            {
                float x = Convert.ToSingle(_length) / (_markerCount - 1) * i - Convert.ToSingle(_length / 2);
                u[i] = _heatConductivity.HeatConductivity(x, _curTime);
            }

            for (int i = 0; i < _markerCount; i++)
            {
                float xWin = Convert.ToSingle(KernelLength * i) / (_markerCount - 1) + (pictureBox1.Width - KernelLength) / 2;

                // температурные маркеры
                RenderMarker(g, xWin, yOffset + cYShift + 24, Convert.ToInt32(u[i]).ToString());

                // маркеры длины
                float xlen = Convert.ToSingle(_length) / (_markerCount - 1) * i;
                RenderLenMarkers(g, xWin, yOffset + cYShift, xlen.ToString("0.00"));

                // линии длины
                g.DrawLine(Pens.Black, xWin, yOffset - 10, xWin, yOffset - 18);
            }

            // прямоугольник для функций
            Rectangle boundRect = GetBoundRect();
            g.DrawRectangle(Pens.DarkGray, boundRect);

            Pen penDash = new Pen(Color.DarkGray)
            {
                DashStyle = System.Drawing.Drawing2D.DashStyle.Dash
            };

            Pen sourcePen = new Pen(Color.DarkBlue, 2.0f);
            g.DrawLines(sourcePen, _indexesSource);

            for (int i = 0; i < _markerCount; i++)
            {
                float xWin = Convert.ToSingle(KernelLength * i) / (_markerCount - 1) + (pictureBox1.Width - KernelLength) / 2;

                if (i > 0 && i < _markerCount - 1)
                {
                    g.DrawLine(penDash, xWin, _cyMargin, xWin, boundRect.Bottom);
                }

                g.FillRectangle(Brushes.DarkBlue, new RectangleF(_indexesSource[i].X - _cRs, _indexesSource[i].Y - _cRs, 2 * _cRs + 1, 2 * _cRs + 1));
            }

            if (_enumWinStatus == EnumWinStatus.Working || _enumWinStatus == EnumWinStatus.Paused)
            {
                for (int i = 0; i < _markerCount; i++)
                {
                    _indexesRunTime[i].X = Convert.ToSingle(KernelLength * i) / (_markerCount - 1) + (pictureBox1.Width - KernelLength) / 2;
                    _indexesRunTime[i].Y = Convert.ToSingle(-boundRect.Height / (_maxt - _mint) * (u[i] - _mint) + boundRect.Bottom);
                }

                Pen runPen = new Pen(Color.Red, 2.0f);
                g.DrawLines(runPen, _indexesRunTime);

                for (int i = 0; i < _markerCount; i++)
                {
                    g.FillRectangle(Brushes.Red, new RectangleF(_indexesRunTime[i].X - _cRs, _indexesRunTime[i].Y - _cRs, 2 * _cRs + 1, 2 * _cRs + 1));
                }
            }
        }

        void Render()
        {
            // проверка картинки на валидность.
            if (_bitmap == null)
                return;

            // создаем новый графический контекст.
            Graphics g = Graphics.FromImage(_bitmap);

            // очищаем контекст.
            g.Clear(Color.White);

            // отрисовка стержня
            RenderKernel(g, _colors, _indexes);

            // отрисовка.
            pictureBox1.Image = _bitmap;
        }

        void BlockControls(bool enable)
        {
            comboBoxMaterials.Enabled = enable;
            txtMinT.Enabled = enable;
            txtMaxT.Enabled = enable;
            txtLen.Enabled = enable;
            btnAddMaterial.Enabled = enable;
            btnAddSplav.Enabled = enable;
            btnRemove.Enabled = enable;
            btnRefresh.Enabled = enable;
        }

        bool IsValid
        {
            get
            {
                bool result = false;
                _lastError = string.Empty;

                do
                {
                    if (!double.TryParse(txtMinT.Text, out _mint))
                    {
                        _lastError = "Минимальная температура задана некорректно.";
                        break;
                    }

                    if (!double.TryParse(txtMaxT.Text, out _maxt))
                    {
                        _lastError = "Максимальная температура задана некорректно.";
                        break;
                    }

                    if (_mint >= _maxt)
                    {
                        _lastError = "Минимальная температура не должны быть меньше максимальной.";
                        break;
                    }

                    Material material = comboBoxMaterials.SelectedItem as Material;

                    if (_maxt >= material.MeltingPoint)
                    {
                        _lastError = $"Максимальгая температура превосходит тепмпратуру плавления {material}.";
                        break;
                    }

                    /*if (!double.TryParse(txtLen.Text, out _length))
                    {
                        _lastError = "Длина стержня задана некорретно.";
                        break;
                    }*/

                    result = true;

                } while (false);

                return result;
            }
        }

        IEnumerable<Material> StockMaterials
        {
            get
            {
                return new List<Material>
                {
                    new Material { Name = "Алюминий", Conductivity = 237, HeatCapacity = 897, Density = 2712, MeltingPoint = 660 },
                    new Material { Name = "Медь", Conductivity = 401, HeatCapacity = 375, Density = 8890, MeltingPoint = 1083 },
                    new Material { Name = "Золото", Conductivity = 318, HeatCapacity = 130, Density = 19300, MeltingPoint = 1064 }
                };
            }
        }

        IEnumerable<Material> LoadMaterials()
        {
            if (!File.Exists(_storage))
                return StockMaterials;

            // передаем в конструктор тип класса
            XmlSerializer formatter = new XmlSerializer(typeof(Material[]));
            IEnumerable<Material> materials;

            // десериализация
            using (FileStream fs = new FileStream(_storage, FileMode.Open))
            {
                materials = (IEnumerable<Material>)formatter.Deserialize(fs);
            }

            return materials.Count() > 0 ? materials : StockMaterials;
        }

        void SaveMaterials(IEnumerable<Material> materials)
        {
            // передаем в конструктор тип класса
            XmlSerializer formatter = new XmlSerializer(typeof(Material[]));

            // получаем поток, куда будем записывать сериализованный объект
            using (FileStream fs = new FileStream(_storage, FileMode.Create))
            {
                formatter.Serialize(fs, materials.ToArray());
            }
        }

        #endregion

        private void btnClose_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            _nfi = System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat;

            _bBlockEvent = true;

            txtMinT.Text = _mint.ToString();
            txtMaxT.Text = _maxt.ToString();

            IEnumerable<Material> materials = LoadMaterials();
            comboBoxMaterials.Items.AddRange(materials.ToArray());
            comboBoxMaterials.SelectedIndex = 0;

            pictureBox1.BackColor = Color.White;

            _indexesSource = GetSourceIndexes(Plateau, _markerCount);

            _bitmap = new Bitmap(pictureBox1.Width, pictureBox1.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
            _curTime = _cStartTime;

            // палитра цветов
            _colors = CreatePalette();
            _indexes = new int[KernelLength];

            _curTime = _cStartTime;

            Material material = comboBoxMaterials.SelectedItem as Material;
            _heatConductivity = new HeatConductivityEquation(-_cIntegralLimit, _cIntegralLimit, material.A2)
            {
                F = HeatFunction0
            };

            ComputeColorIndexes();

            _bBlockEvent = false;
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            List<Material> materials = new List<Material>();

            foreach(Material m in comboBoxMaterials.Items)
            {
                materials.Add(m);
            }

            SaveMaterials(materials);
        }

        private void comboBoxMaterials_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (comboBoxMaterials.SelectedIndex < 0)
                return;

            Material material = comboBoxMaterials.SelectedItem as Material;

            txtLambda.Text = material.Conductivity.ToString();
            txtCp.Text = material.HeatCapacity.ToString();
            txtRo.Text = material.Density.ToString();
            txtMeltingPoint.Text = material.MeltingPoint.ToString();
        }

        private void btnAddMaterial_Click(object sender, EventArgs e)
        {
            frmNewMaterial dlg = new frmNewMaterial();

            if (dlg.ShowDialog() == DialogResult.OK)
            {
                Material material = dlg.Material;

                comboBoxMaterials.Items.Add(material);
                comboBoxMaterials.SelectedIndex = comboBoxMaterials.Items.Count - 1;
            }
        }

        private void btnRemove_Click(object sender, EventArgs e)
        {
            if (comboBoxMaterials.SelectedIndex < 0)
                return;

            var item = comboBoxMaterials.SelectedItem;
            string mes = $"Вы действительно хотите удалить {item}?";
            
            if (MessageBox.Show(mes, "Удаление материала", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
            {
                comboBoxMaterials.Items.Remove(item);

                if (comboBoxMaterials.Items.Count > 0)
                {
                    comboBoxMaterials.SelectedIndex = 0;
                }
            }
        }

        private void btnAddSplav_Click(object sender, EventArgs e)
        {
            Material[] materials = new Material[comboBoxMaterials.Items.Count];
            comboBoxMaterials.Items.CopyTo(materials, 0);

            FormSplav dlg = new FormSplav(materials);

            if (dlg.ShowDialog() == DialogResult.OK)
            {
                Material splav = dlg.Splav;

                comboBoxMaterials.Items.Add(splav);
                comboBoxMaterials.SelectedIndex = comboBoxMaterials.Items.Count - 1;
            }
        }

        private void btnStart_Click(object sender, EventArgs e)
        {
            if (!IsValid)
            {
                MessageBox.Show(_lastError, "Ошибка в стартовых параметрах", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                return;
            }

            if (_enumWinStatus == EnumWinStatus.Stoped)
            {
                _curTime = _cStartTime;
            }

            if (_enumWinStatus == EnumWinStatus.Stoped || _enumWinStatus == EnumWinStatus.Paused)
            {
                BlockControls(false);

                _indexesRunTime = new PointF[_indexesSource.Length];
                Array.Copy(_indexesSource, _indexesRunTime, _indexesRunTime.Length);

                _startTime = DateTime.Now - _timeSpan;
                _timer1 = new Timer 
                { 
                    Interval = 25,
                    Enabled = false
                };
                _timer1.Tick += OnTimer1Tick;
                _timer1.Start();

                _timer2 = new Timer
                {
                    Interval = Convert.ToInt32(_cTimerInterval),
                    Enabled = false
                };
                _timer2.Tick += OnTimer2Tick;
                _timer2.Start();

                _enumWinStatus = EnumWinStatus.Working;
            }
        }

        private void btnPause_Click(object sender, EventArgs e)
        {
            if (_enumWinStatus == EnumWinStatus.Working)
            {
                _timer1.Stop();
                _timer2.Stop();
                _enumWinStatus = EnumWinStatus.Paused;
            }
        }

        private void btnStop_Click(object sender, EventArgs e)
        {
            if (_enumWinStatus == EnumWinStatus.Working || _enumWinStatus == EnumWinStatus.Paused)
            {
                BlockControls(true);
                _timer1.Stop();
                _timer2.Stop();
                _timeSpan = new TimeSpan(0);
                lblTime.Text = "0.00:00";
                _enumWinStatus = EnumWinStatus.Stoped;

                _curTime = _cStartTime;
                ComputeColorIndexes();
                Render();
            }
        }

        private void OnTimer1Tick(object sender, EventArgs e)
        {
            DateTime curDateTime = DateTime.Now;
            _timeSpan = curDateTime - _startTime;
            lblTime.Text = string.Format("{0}.{1}:{2}", _timeSpan.Hours, _timeSpan.Minutes.ToString("00"), _timeSpan.Seconds.ToString("00"));
        }

        private void OnTimer2Tick(object sender, EventArgs e)
        {
            Next();
            Render();
        }

        private void OnDoubleCheck(object sender, KeyPressEventArgs e)
        {
            if (!(char.IsDigit(e.KeyChar) || e.KeyChar == Convert.ToChar(Keys.Back) || e.KeyChar == _nfi.NumberDecimalSeparator[0]))
            {
                e.Handled = true;
            }
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            Render();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Form dlg = new FormAbout();
            dlg.ShowDialog();
        }

        private void cmbMarkers_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (_bBlockEvent)
                return;

            _indexesSource = GetSourceIndexes(HeatFunction0, _markerCount);
            _heatConductivity.F = HeatFunction0;
            ComputeColorIndexes();

            Render();
        }

        private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
        {
            if (_enumWinStatus == EnumWinStatus.Stoped)
            {
                if (e.Button == MouseButtons.Left)
                {
                    RectangleF[] rectangleFs = _indexesSource.Select(p => new RectangleF(p.X - _cRs, p.Y - _cRs, 2 * _cRs + 1, 2 * _cRs + 1)).ToArray();
                    RectangleF ri = rectangleFs.FirstOrDefault(r => r.Contains(e.X, e.Y));

                    _curSourceIndex = Array.IndexOf(rectangleFs, ri);
                }
            }            
        }

        private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            if (_enumWinStatus == EnumWinStatus.Stoped)
            {
                if (e.Button == MouseButtons.Left)
                {
                    if (_curSourceIndex >= 0)
                    {
                        _indexesSource[_curSourceIndex].Y = e.Y;

                        // коррекция значений
                        Rectangle bound = GetBoundRect();

                        if (_indexesSource[_curSourceIndex].Y > bound.Bottom)
                        {
                            _indexesSource[_curSourceIndex].Y = bound.Bottom;
                        }

                        if (_indexesSource[_curSourceIndex].Y < bound.Top)
                        {
                            _indexesSource[_curSourceIndex].Y = bound.Top;
                        }

                        _heatConductivity.F = HeatFunction0;
                        ComputeColorIndexes();

                        Render();
                    }
                }
            }
        }

        private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
        {
            if (_enumWinStatus == EnumWinStatus.Stoped)
            {
                if (e.Button == MouseButtons.Left)
                {
                    _curSourceIndex = -1;
                }
            }
        }

        private void btnRefresh_Click(object sender, EventArgs e)
        {
            if (!IsValid)
            {
                MessageBox.Show(_lastError, "Ошибка в стартовых параметрах", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                return;
            }

            _heatConductivity.F = HeatFunction0;
            ComputeColorIndexes();
            Render();
        }
    }
}
